// TODO don't let mouse escape scrollbar while scrolling
// TODO line/word wrap
// TODO redundant draws on scrollbar drag and arrow up/down
// TODO don't load entire screen on non-scrolling updates
// TODO get rid of goplan9 EOF error
// TODO migrate internal data structures to strings?
// TODO vet len(string) type code and ensure unicode still works
// TODO use compressed load?

package text

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"io"
	"io/ioutil"
	"sync"
	"time"

	"code.google.com/p/jnj/graphics"
	"code.google.com/p/jnj/graphics/widgets"
	"code.google.com/p/jnj/ttf"
)

var margin = image.Point{4, 0}

type Options struct {
	Rect         image.Rectangle
	BGCol        color.Color
	TextCol      color.Color
	SelCol       color.Color
	FontPath     string
	KeyboardChan chan rune
	MouseChan    chan *graphics.MouseEvent
	ClipChan     chan string
	Tabwidth     int
}

type Buffer struct {
	DrawChan     chan *image.RGBA // image updates for client to draw
	ClickChan    chan *Address    // the Address affected by the most recent click
	MouseChan    chan *graphics.MouseEvent
	KeyboardChan chan rune
	ClipChan     chan string

	img     *image.RGBA
	r       image.Rectangle // does not include scrollbar or margin
	bgcol   *image.Uniform
	textcol *image.Uniform
	selcol  *image.Uniform
	cursor  *image.RGBA
	font    *ttf.Font

	// double-click state information
	dclick      bool
	dclicktimer *time.Timer

	scrollpt  image.Point // in pixels
	scrollbar *widgets.Scrollbar

	lines []*line
	Dot   Address

	// undo/redo
	currentState *state

	drawlock sync.Mutex
}

func NewBuffer(opts Options) (*Buffer, error) {
	font, err := ttf.NewFont(opts.FontPath)
	if err != nil {
		return nil, fmt.Errorf("error loading font \"%s\"", opts.FontPath)
	}
	font.SetHinting(ttf.FullHinting)

	lines := make([]*line, 1, 100)
	lines[0] = &line{
		s:     make([]rune, 0, 100),
		dirty: true,
	}

	if opts.BGCol == nil {
		opts.BGCol = color.RGBA{R: 0xFF, G: 0xFF, B: 0xEA, A: 0xFF}
	}
	bgcol := image.NewUniform(opts.BGCol)
	if opts.BGCol == nil {
		opts.BGCol = image.White
	}
	if opts.SelCol == nil {
		opts.SelCol = color.RGBA{R: 0xEE, G: 0xEE, B: 0x9E, A: 0xFF}
	}
	if opts.Tabwidth == 0 {
		opts.Tabwidth = 4
	}

	cursor := image.NewRGBA(image.Rect(0, 0, 3, font.Height))
	draw.Draw(cursor, cursor.Rect, image.Black, image.ZP, draw.Src)
	h := font.Height - 3 // TODO magic 3
	draw.Draw(cursor, image.Rect(0, 3, 1, h), bgcol, image.ZP, draw.Src)
	draw.Draw(cursor, image.Rect(2, 3, 3, h), bgcol, image.ZP, draw.Src)

	img := image.NewRGBA(opts.Rect)
	draw.Draw(img, opts.Rect, bgcol, image.ZP, draw.Src)
	b := &Buffer{
		DrawChan:     make(chan *image.RGBA, 10),
		ClickChan:    make(chan *Address),
		img:          img,
		bgcol:        bgcol,
		textcol:      image.NewUniform(opts.TextCol),
		selcol:       image.NewUniform(opts.SelCol),
		cursor:       cursor,
		font:         font,
		lines:        lines,
		Dot:          Address{},
		MouseChan:    opts.MouseChan,
		KeyboardChan: opts.KeyboardChan,
		ClipChan:     opts.ClipChan,
	}
	b.scrollbar = widgets.NewScrollbar(b)

	b.r = opts.Rect
	b.r.Min = b.r.Min.Add(margin)
	b.r.Min.X += b.scrollbar.Bounds().Dx()

	go b.domouse()
	go b.dokbd()

	//b.redraw() // TODO: do we need this?

	return b, nil
}

// TODO implement
func (b *Buffer) Close() {
}

// Bounds returns an image.Rectangle representing the dimensions of the buffer
// in pixels.
func (b *Buffer) Bounds() image.Rectangle {
	return b.img.Rect
}

// Clear empties b.
func (b *Buffer) Clear() {
	b.selall()
	b.deletesel()
}

// Load reads r into a string and calls LoadString.
func (b *Buffer) Load(r io.Reader) (Address, error) {
	s, err := ioutil.ReadAll(r)
	if err != nil {
		return Address{}, err
	}

	// b.LoadString does a redraw.
	return b.LoadString(string(s)), nil
}

// LoadString replaces the contents of b with s, and places dot at (0,0)
func (b *Buffer) LoadString(s string) Address {
	b.selall()
	p := b.load(s)
	b.redraw()
	return Address{
		Left:      image.ZP,
		Right:     p,
		selection: selLeft,
	}
}

func (b *Buffer) Selection() string {
	return b.getsel()
}

// Contenst returns a slice containing a string for each line in the Buffer.
func (b *Buffer) Contents() []string {
	c := make([]string, len(b.lines))
	for i, l := range b.lines {
		c[i] = string(l.s)
	}
	return c
}

// TODO move this logic somewhere else?
func (b *Buffer) Select(a Address) {
	if a.Left == a.Right {
		a.selection = selNone
	} else {
		a.selection = selLeft
	}
	y := a.Left.Y * b.font.Height
	top := b.scrollpt.Y
	bottom := b.scrollpt.Y + b.Bounds().Dy() - 1
	if y <= top || y >= bottom {
		// the new selection is not visible, scroll until it is in the top quarter
		// of the visible portion of the buffer.
		lines := b.Bounds().Dy() / b.font.Height
		b.scrollpt = image.ZP
		b.scroll(a.Left.Y - int(float32(lines)/4.0))
	}
	b.Dot = a
	b.redraw()
}

// PointAtAddress returns the pixel coordinates of the top left corner of a.
func (b *Buffer) PointAtAddress(a Address) image.Point {
	x := b.getxpx(a.Left.Y, a.Left.X) + 3
	y := b.getypx(a.Left.Y) + b.font.Height - 3
	return image.Pt(x, y)
}

func (b *Buffer) StringAtAddress(a Address) string {
	s := string(b.lines[a.Left.Y].s)
	if a.Right.Y > a.Left.Y {
		s = s[a.Left.X:] + "\n"
		for l := a.Left.Y; l < a.Right.Y; l++ {
			s += string(b.lines[l].s) + "\n"
		}
		s += string(b.lines[a.Right.Y].s)[:a.Right.X]
	} else {
		s = s[a.Left.X:a.Right.X]
	}
	return s
}

func (b *Buffer) Resize(r image.Rectangle) {
	b.r = r
	b.r.Min = b.r.Min.Add(margin)
	b.r.Min.X += b.scrollbar.Bounds().Dx()

	b.img = image.NewRGBA(r)
	draw.Draw(b.img, b.r, b.bgcol, image.ZP, draw.Src)
	for _, line := range b.lines {
		line.dirty = true
	}

	b.scrollbar = widgets.NewScrollbar(b)

	go func() {
		b.redraw()
	}()
}

func (b *Buffer) Scroll(step int) {
	b.scroll(step)
}

func (b *Buffer) ScrollTo(pos int) {
	b.scrollto(pos)
}

func (b *Buffer) ScrollState() (current, max, stepsize int) {
	return b.scrollpt.Y / b.font.Height, len(b.lines), b.font.Height
}

func (b *Buffer) CanUndo() bool {
	if b.currentState == nil {
		return false
	}
	return b.currentState.prev != nil
}

func (b *Buffer) Undo() {
	b.undo()
	b.redraw()
}

func (b *Buffer) CanRedo() bool {
	if b.currentState == nil {
		return false
	}
	return b.currentState.next != nil
}

func (b *Buffer) Redo() {
	b.redo()
	b.redraw()
}
